// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/ssl/ssl_client_session_cache.h"

#include <openssl/ssl.h>

#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/simple_test_clock.h"
#include "net/ssl/scoped_openssl_types.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace net {

// Test basic insertion and lookup operations.
TEST(SSLClientSessionCacheTest, Basic)
{
    SSLClientSessionCache::Config config;
    SSLClientSessionCache cache(config);

    ScopedSSL_SESSION session1(SSL_SESSION_new());
    ScopedSSL_SESSION session2(SSL_SESSION_new());
    ScopedSSL_SESSION session3(SSL_SESSION_new());
    EXPECT_EQ(1u, session1->references);
    EXPECT_EQ(1u, session2->references);
    EXPECT_EQ(1u, session3->references);

    EXPECT_EQ(nullptr, cache.Lookup("key1").get());
    EXPECT_EQ(nullptr, cache.Lookup("key2").get());
    EXPECT_EQ(0u, cache.size());

    cache.Insert("key1", session1.get());
    EXPECT_EQ(session1.get(), cache.Lookup("key1").get());
    EXPECT_EQ(nullptr, cache.Lookup("key2").get());
    EXPECT_EQ(1u, cache.size());

    cache.Insert("key2", session2.get());
    EXPECT_EQ(session1.get(), cache.Lookup("key1").get());
    EXPECT_EQ(session2.get(), cache.Lookup("key2").get());
    EXPECT_EQ(2u, cache.size());

    EXPECT_EQ(2u, session1->references);
    EXPECT_EQ(2u, session2->references);

    cache.Insert("key1", session3.get());
    EXPECT_EQ(session3.get(), cache.Lookup("key1").get());
    EXPECT_EQ(session2.get(), cache.Lookup("key2").get());
    EXPECT_EQ(2u, cache.size());

    EXPECT_EQ(1u, session1->references);
    EXPECT_EQ(2u, session2->references);
    EXPECT_EQ(2u, session3->references);

    cache.Flush();
    EXPECT_EQ(nullptr, cache.Lookup("key1").get());
    EXPECT_EQ(nullptr, cache.Lookup("key2").get());
    EXPECT_EQ(nullptr, cache.Lookup("key3").get());
    EXPECT_EQ(0u, cache.size());

    EXPECT_EQ(1u, session1->references);
    EXPECT_EQ(1u, session2->references);
    EXPECT_EQ(1u, session3->references);
}

// Test that a session may be inserted at two different keys. This should never
// be necessary, but the API doesn't prohibit it.
TEST(SSLClientSessionCacheTest, DoubleInsert)
{
    SSLClientSessionCache::Config config;
    SSLClientSessionCache cache(config);

    ScopedSSL_SESSION session(SSL_SESSION_new());
    EXPECT_EQ(1u, session->references);

    EXPECT_EQ(nullptr, cache.Lookup("key1").get());
    EXPECT_EQ(nullptr, cache.Lookup("key2").get());
    EXPECT_EQ(0u, cache.size());

    cache.Insert("key1", session.get());
    EXPECT_EQ(session.get(), cache.Lookup("key1").get());
    EXPECT_EQ(nullptr, cache.Lookup("key2").get());
    EXPECT_EQ(1u, cache.size());

    EXPECT_EQ(2u, session->references);

    cache.Insert("key2", session.get());
    EXPECT_EQ(session.get(), cache.Lookup("key1").get());
    EXPECT_EQ(session.get(), cache.Lookup("key2").get());
    EXPECT_EQ(2u, cache.size());

    EXPECT_EQ(3u, session->references);

    cache.Flush();
    EXPECT_EQ(nullptr, cache.Lookup("key1").get());
    EXPECT_EQ(nullptr, cache.Lookup("key2").get());
    EXPECT_EQ(0u, cache.size());

    EXPECT_EQ(1u, session->references);
}

// Tests that the session cache's size is correctly bounded.
TEST(SSLClientSessionCacheTest, MaxEntries)
{
    SSLClientSessionCache::Config config;
    config.max_entries = 3;
    SSLClientSessionCache cache(config);

    ScopedSSL_SESSION session1(SSL_SESSION_new());
    ScopedSSL_SESSION session2(SSL_SESSION_new());
    ScopedSSL_SESSION session3(SSL_SESSION_new());
    ScopedSSL_SESSION session4(SSL_SESSION_new());

    // Insert three entries.
    cache.Insert("key1", session1.get());
    cache.Insert("key2", session2.get());
    cache.Insert("key3", session3.get());
    EXPECT_EQ(session1.get(), cache.Lookup("key1").get());
    EXPECT_EQ(session2.get(), cache.Lookup("key2").get());
    EXPECT_EQ(session3.get(), cache.Lookup("key3").get());
    EXPECT_EQ(3u, cache.size());

    // On insertion of a fourth, the first is removed.
    cache.Insert("key4", session4.get());
    EXPECT_EQ(nullptr, cache.Lookup("key1").get());
    EXPECT_EQ(session4.get(), cache.Lookup("key4").get());
    EXPECT_EQ(session3.get(), cache.Lookup("key3").get());
    EXPECT_EQ(session2.get(), cache.Lookup("key2").get());
    EXPECT_EQ(3u, cache.size());

    // Despite being newest, the next to be removed is session4 as it was accessed
    // least. recently.
    cache.Insert("key1", session1.get());
    EXPECT_EQ(session1.get(), cache.Lookup("key1").get());
    EXPECT_EQ(session2.get(), cache.Lookup("key2").get());
    EXPECT_EQ(session3.get(), cache.Lookup("key3").get());
    EXPECT_EQ(nullptr, cache.Lookup("key4").get());
    EXPECT_EQ(3u, cache.size());
}

// Tests that session expiration works properly.
TEST(SSLClientSessionCacheTest, Expiration)
{
    const size_t kNumEntries = 20;
    const size_t kExpirationCheckCount = 10;
    const base::TimeDelta kTimeout = base::TimeDelta::FromSeconds(1000);

    SSLClientSessionCache::Config config;
    config.expiration_check_count = kExpirationCheckCount;
    config.timeout = kTimeout;
    SSLClientSessionCache cache(config);
    base::SimpleTestClock* clock = new base::SimpleTestClock;
    cache.SetClockForTesting(base::WrapUnique(clock));

    // Add |kNumEntries - 1| entries.
    for (size_t i = 0; i < kNumEntries - 1; i++) {
        ScopedSSL_SESSION session(SSL_SESSION_new());
        cache.Insert(base::SizeTToString(i), session.get());
    }
    EXPECT_EQ(kNumEntries - 1, cache.size());

    // Expire all the previous entries and insert one more entry.
    clock->Advance(kTimeout * 2);
    ScopedSSL_SESSION session(SSL_SESSION_new());
    cache.Insert("key", session.get());

    // All entries are still in the cache.
    EXPECT_EQ(kNumEntries, cache.size());

    // Perform one fewer lookup than needed to trigger the expiration check. This
    // shall not expire any session.
    for (size_t i = 0; i < kExpirationCheckCount - 1; i++)
        cache.Lookup("key");

    // All entries are still in the cache.
    EXPECT_EQ(kNumEntries, cache.size());

    // Perform one more lookup. This will expire all sessions but the last one.
    cache.Lookup("key");
    EXPECT_EQ(1u, cache.size());
    EXPECT_EQ(session.get(), cache.Lookup("key").get());
    for (size_t i = 0; i < kNumEntries - 1; i++) {
        SCOPED_TRACE(i);
        EXPECT_EQ(nullptr, cache.Lookup(base::SizeTToString(i)));
    }
}

// Tests that Lookup performs an expiration check before returning a cached
// session.
TEST(SSLClientSessionCacheTest, LookupExpirationCheck)
{
    // kExpirationCheckCount is set to a suitably large number so the automated
    // pruning never triggers.
    const size_t kExpirationCheckCount = 1000;
    const base::TimeDelta kTimeout = base::TimeDelta::FromSeconds(1000);

    SSLClientSessionCache::Config config;
    config.expiration_check_count = kExpirationCheckCount;
    config.timeout = kTimeout;
    SSLClientSessionCache cache(config);
    base::SimpleTestClock* clock = new base::SimpleTestClock;
    cache.SetClockForTesting(base::WrapUnique(clock));

    // Insert an entry into the session cache.
    ScopedSSL_SESSION session(SSL_SESSION_new());
    cache.Insert("key", session.get());
    EXPECT_EQ(session.get(), cache.Lookup("key").get());
    EXPECT_EQ(1u, cache.size());

    // Expire the session.
    clock->Advance(kTimeout * 2);

    // The entry has not been removed yet.
    EXPECT_EQ(1u, cache.size());

    // But it will not be returned on lookup and gets pruned at that point.
    EXPECT_EQ(nullptr, cache.Lookup("key").get());
    EXPECT_EQ(0u, cache.size());

    // Sessions also are treated as expired if the clock rewinds.
    cache.Insert("key", session.get());
    EXPECT_EQ(session.get(), cache.Lookup("key").get());
    EXPECT_EQ(1u, cache.size());

    clock->Advance(-kTimeout * 2);

    EXPECT_EQ(nullptr, cache.Lookup("key").get());
    EXPECT_EQ(0u, cache.size());
}

} // namespace net
